package com.benshou.bcss.common.log;

import com.alibaba.ttl.TransmittableThreadLocal;
import com.benshou.bcss.common.j17common.annotation.NotNull;
import com.benshou.bcss.common.trace.TraceIdContext;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.ModelAndView;

/**
 * Web 日志打印切面处理
 *
 * @author <a href="mailto:liuzhenbang@benshou.com">Liu Zhenbang</a>
 */
@Aspect
@Component
@Slf4j
@RequiredArgsConstructor
public class WebLogAspect {

    /**
     * 对象数据在日志中的打印最长长度
     */
    private static final int MAX_LEN_LOG_OBJECT_STRING = 5000;

    /**
     * 线程本地变量：开始时间戳
     */
    private static final ThreadLocal<Long> TL_START_TIMESTAMP = new TransmittableThreadLocal<>();

    /**
     * 对象映射实例
     */
    private final ObjectMapper objectMapper;

    /**
     * 切点：被 {@link WebLog} 注解的地方
     */
    @Pointcut("@annotation(com.benshou.bcss.common.log.WebLog)")
    public void webLogAnnotated() {

    }

    /**
     * 切点： Controller 中的方法
     */
    @Pointcut("within(@org.springframework.stereotype.Controller *) || " +
            "within(@org.springframework.web.bind.annotation.RestController *)")
    public void controllerMethod() {

    }

    /**
     * WebLog 总切点
     */
    @Pointcut("webLogAnnotated() || controllerMethod()")
    public void webLog() {
    }

    /**
     * 执行前的操作
     *
     * @param joinPoint 切点数据
     */
    @Before("webLog()")
    public void doBefore(JoinPoint joinPoint) {
        try {
            // 打印调用内容
            if (!logger.isDebugEnabled()) {
                return;
            }

            TL_START_TIMESTAMP.set(System.currentTimeMillis());
            logger.debug("""
                    \n-----------------------------开始调用-----------------------------
                    TraceId   : {}
                    Controller: {}
                    Method    : {}
                    Params    : \\{{}\\}
                    ----------------------------------------------------------------
                    """.stripIndent(),
                    TraceIdContext.get(),
                    joinPoint.getSignature().getDeclaringType(),
                    joinPoint.getSignature().getName(),
                    buildParamsLog(joinPoint));
        } catch (Exception e) {
            logger.warn("Exception: ", e);
        }
    }

    /**
     * 执行结果返回后的操作
     *
     * @param joinPoint 切点数据
     * @param returning 返回的结果
     */
    @AfterReturning(pointcut = "webLog()", returning = "returning")
    public void doAfterReturning(JoinPoint joinPoint, Object returning) {
        try {
            // 打印服务结果
            if (!logger.isDebugEnabled()) {
                return;
            }

            // 计算用时
            var start = TL_START_TIMESTAMP.get();
            String costLog;
            if (start == null) {
                costLog = "LOST";
            } else {
                var cost = System.currentTimeMillis() - start;
                TL_START_TIMESTAMP.remove();
                costLog = cost + "ms";
            }

            logger.debug("""
                    \n-----------------------------结束调用-----------------------------
                    TraceId   : {}
                    Controller: {}
                    Method    : {}
                    CostTime  : {}
                    Result    : {}
                    ----------------------------------------------------------------
                    """.stripIndent(),
                    TraceIdContext.get(),
                    joinPoint.getSignature().getDeclaringType(),
                    joinPoint.getSignature().getName(),
                    costLog,
                    cutoff(this.objectMapper.writeValueAsString(returning)));
        } catch (Exception e) {
            logger.warn("Exception: ", e);
        }
    }

    /**
     * 构建切点参数日志
     *
     * @param joinPoint 切点信息
     * @return 日志
     */
    private String buildParamsLog(@NotNull JoinPoint joinPoint) {
        try {
            var logBuilder = new StringBuilder(MAX_LEN_LOG_OBJECT_STRING);
            for (var arg : joinPoint.getArgs()) {
                if (arg instanceof ServletRequest ||
                        arg instanceof ServletResponse ||
                        arg instanceof MultipartFile ||
                        arg instanceof ModelAndView ||
                        arg instanceof Model ||
                        arg instanceof BindingResult) {
                    continue;
                }

                logBuilder.append("\n    ").append(this.objectMapper.writeValueAsString(arg)).append(",");
                if (logBuilder.length() >= MAX_LEN_LOG_OBJECT_STRING) {
                    return logBuilder.substring(0, MAX_LEN_LOG_OBJECT_STRING) + "...";
                }
            }

            return cutoff(logBuilder.toString());
        } catch (JsonProcessingException e) {
            logger.warn("Exception: ", e);
            return "EXCEPTION";
        }
    }

    /**
     * 截断内容
     *
     * @param string 内容
     * @return 截断后的内容
     */
    private static String cutoff(@NotNull String string) {
        return string.length() > MAX_LEN_LOG_OBJECT_STRING ?
                string.substring(0, MAX_LEN_LOG_OBJECT_STRING) :
                string;
    }
}
